refactor(membership): deduplicate repeated patterns - #1827
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe membership service centralizes policy filtering, role validation, typed principal handling, role enrichment, policy cleanup, and audit writes across organization, group, and project membership operations. ChangesMembership Refactor
Estimated code review effort: 3 (Moderate) | ~30 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
9f90779 to
42a6da4
Compare
Coverage Report for CI Build 30536703296Coverage decreased (-0.1%) to 46.99%Details
Uncovered Changes
Coverage Regressions1 previously-covered line in 1 file lost coverage.
Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
core/membership/list.go (1)
56-152: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAvoid a redundant duplicate policy list when no role filter is applied.
When
filter.RoleIDsis empty,fltpassed intoenrichMemberRoles(line 85) is identical to the filter already used at line 64 to fetchpolicies, yetenrichMemberRolesunconditionally re-queriess.policyService.Listwith that same filter (line 99). This doubles the DB/service round trip on the common "list all members, no role filter" path.Consider reusing the already-fetched
policiesfor enrichment whenfilter.RoleIDsis empty, only re-listing when a role filter actually narrowed the initial selection.♻️ Sketch of the optimization
func (s *Service) ListPrincipalsByResource(ctx context.Context, resourceID, resourceType string, filter MemberFilter) ([]Member, error) { flt, err := resourcePolicyFilter(resourceID, resourceType) if err != nil { return nil, err } flt.PrincipalType = filter.PrincipalType flt.RoleIDs = filter.RoleIDs policies, err := s.policyService.List(ctx, flt) if err != nil { return nil, fmt.Errorf("list policies: %w", err) } policies = excludePATAllProjects(policies, resourceType) ... - if err := s.enrichMemberRoles(ctx, flt, resourceType, memberIndex, members); err != nil { + var reusable []policy.Policy + if len(filter.RoleIDs) == 0 { + reusable = policies // already the full unfiltered set + } + if err := s.enrichMemberRoles(ctx, flt, resourceType, memberIndex, members, reusable); err != nil { return nil, err } return members, nil } func (s *Service) enrichMemberRoles(ctx context.Context, flt policy.Filter, resourceType string, memberIndex map[principalKey]int, members []Member, allPolicies []policy.Policy) error { - flt.RoleIDs = nil - allPolicies, err := s.policyService.List(ctx, flt) - if err != nil { - return fmt.Errorf("list policies for role enrichment: %w", err) + if allPolicies == nil { + flt.RoleIDs = nil + var err error + allPolicies, err = s.policyService.List(ctx, flt) + if err != nil { + return fmt.Errorf("list policies for role enrichment: %w", err) + } + allPolicies = excludePATAllProjects(allPolicies, resourceType) } - allPolicies = excludePATAllProjects(allPolicies, resourceType) ...Note: existing unit tests pinned to two identical
Listcalls (e.g.core/membership/list_test.go) would need updating alongside this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d895a0ae-fc74-436b-9c66-c58eaaaa0a8c
📒 Files selected for processing (7)
core/membership/audit.gocore/membership/group.gocore/membership/list.gocore/membership/org.gocore/membership/project.gocore/membership/project_test.gocore/membership/service.go
|
Applied the CodeRabbit suggestion in c3464d7: |
|
Tested every code path this PR touches, on a live server. No behavior changes found. Setup
Unit tests pass ( |
Behavior unchanged; call sites and public signatures stay the same. - One resourcePolicyFilter builds resource-scoped policy filters; the duplicate policyFilterForResource is gone. removeAllPolicies now goes through it and reuses removePoliciesByFilter, so its filter also pins resource_type (redundant with the per-type ID fields in SQL). - validateOrgRole/validateProjectRole/validateGroupRole are one-line wrappers over a shared validateRoleForScope. - validateMinOwnerConstraint/validateMinGroupOwnerConstraint are wrappers over a shared validateMinRoleConstraint. - The five near-identical org/group audit helpers delegate to one auditMemberChange that writes both audit stores. - hasExactlyRole names the repeated "already has exactly this role" check. - principalKey struct replaces the \x00-joined string map keys, and ListPrincipalsByResource's role enrichment moves into its own function. - listOrgsForPrincipal/listGroupsForPrincipal reuse policyResourceIDs instead of hand-rolled pluck loops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… is set Review follow-up (CodeRabbit): with no role filter, the enrichment query in ListPrincipalsByResource was identical to the member query, costing a second round trip on the common path. Reuse the first result; only a role-filtered listing issues the second, unfiltered query. enrichMemberRoles now takes the policy set instead of fetching it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c3464d7 to
f60c879
Compare
- validateMinRoleConstraint wraps its errors with the guard role name, so org and group owner lookups stay distinguishable in logs - RemoveAllGroupMembers tracks principals in a set; the relation pass reads the identity from the key instead of an arbitrary policy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What this PR does
It removes duplicated code inside the
core/membershippackage. It builds on #1826, which split the one big service file into smaller files. With the code side by side, the copies were easy to see.No results change. Every public function keeps its name and signature. The package shrinks by 165 lines. One function makes one less database query (change #9, from review).
The duplications, one by one
1. Two functions built the same policy filter.
resourcePolicyFilter(list.go) andpolicyFilterForResource(service.go) did the same job: take a resource ID and type, and set the matching field onpolicy.Filter(OrgID,ProjectID, orGroupID). Now onlyresourcePolicyFilterexists.removeAllPoliciesuses it, and then calls the existingremovePoliciesByFilterinstead of keeping its own copy of the list-then-delete loop.One side effect to know about: the filter built by
removeAllPoliciesnow also setsResourceType. The query result is the same. The repository already pins the resource type wheneverOrgID/ProjectID/GroupIDis set (seeapplyListFilterininternal/store/postgres/policy_repository.go), so the new condition is redundant, not different.2. Three role validators were the same function.
validateOrgRole,validateProjectRole, andvalidateGroupRoleall did the same three steps: fetch the role, check it is scoped to the right namespace, then accept it if it is a platform-wide role or a custom role of this org. Only the namespace and the returned error differed. The shared steps now live invalidateRoleForScope. The three old functions stay as one-line wrappers, so call sites read the same as before.3. Two "don't remove the last owner" checks were the same function.
validateMinOwnerConstraint(org) andvalidateMinGroupOwnerConstraint(group) ran the same algorithm: fetch the owner role, skip the check if the member is being promoted to owner or isn't an owner, otherwise count the owner policies and fail if this member is the only one. Both now call the sharedvalidateMinRoleConstraint. Each wrapper passes its own owner role name, filter, and error.4. Five audit helpers repeated the same ~30 lines.
The org and group helpers for member added, role changed, and member removed each built the same audit record (target, metadata with
role_idandemail) and then wrote the same legacy audit log. That shared body is now one function,auditMemberChange. The five helpers became short wrappers that only supply the event names and attributes. Adding the next membership event is a five-line wrapper.5. A repeated condition got a name.
len(existing) == 1 && existing[0].RoleID == roleIDappeared at four call sites. It now readshasExactlyRole(existing, roleID), which says what the check means: the member already has exactly this role, so there is nothing to change.6. String-glued map keys became a struct.
Two places built map keys by joining strings:
principalType + "\x00" + principalID. They now use a small struct,principalKey{Type, ID}. Go structs work directly as map keys, and there is no separator character to reason about.7. The longest function got split.
ListPrincipalsByResourcewas ~85 lines doing two jobs: find the members, then fetch every role each member holds. The second job moved into its own function,enrichMemberRoles.8. Hand-written loops replaced by an existing helper.
listOrgsForPrincipalandlistGroupsForPrincipaleach looped over policies to collect resource IDs and dedupe them. The helperpolicyResourceIDsalready did exactly that, so they call it now.9. One less query in
ListPrincipalsByResource(from review).CodeRabbit spotted a pre-existing inefficiency that the split made visible: when the caller passes no role filter, the role-enrichment query was identical to the member query, so the function hit the database twice for the same rows. Now the first result is reused; only a role-filtered listing issues the second, unfiltered query. Same results, one query instead of two on the common path.
What I looked at but did not merge
validateOrgMembershipandvalidatePrincipaloverlap, but they return different errors (ErrNotOrgMembervsErrPrincipalNotInOrg), and only one of them checks whether a service user is disabled. Merging them would change behavior, and this PR promises not to. Left as is.Tests
Two kinds of test changes, both matching the changes above:
TestService_RemoveProjectMembernow expectResourceType: schema.ProjectNamespacein the filter (change feat: add username and groupname columns #1).TestService_ListPrincipalsByResourcenow pin the policyListcall with.Once()instead of.Times(2)(change chore(deps): bump normalize-url from 4.5.0 to 4.5.1 #9), proving the duplicate query is gone.Everything else passes untouched.
go test ./core/... ./internal/api/...is green.🤖 Generated with Claude Code